Python 3.7中dataclass的终极指南(一)
from dataclasses import dataclass
from typing import List
class PlayingCard:
rank: str
suit: str
class Deck:
#Deck:一副牌。cards参数传入列表,该列表可以含有多个PlayingCard类实例。
cards: List[PlayingCard]
dataclass类默认值(进阶)
假设我们要给Deck提供默认值。 例如,如果Deck()创建了52张扑克牌的常规(法国)牌组。
首先,指定不同的点数和花色。 然后,添加一个函数make_french_deck(),它创建一个PlayingCard实例列表:
RANKS = '2 3 4 5 6 7 8 9 10 J Q K A'.split()
SUITS = '♣ ♢ ♡ ♠'.split()
def make_french_deck():
return [PlayingCard(r, s) for s in SUITS for r in RANKS]
#生成一副52张的扑克牌组合
make_french_deck()
运行
[PlayingCard(rank='2', suit='♣'), PlayingCard(rank='3', suit='♣'), PlayingCard(rank='4', suit='♣'), PlayingCard(rank='5', suit='♣'), PlayingCard(rank='6', suit='♣'),
...
PlayingCard(rank='J', suit='♠'), PlayingCard(rank='Q', suit='♠'), PlayingCard(rank='K', suit='♠'), PlayingCard(rank='A', suit='♠')]
理论上,我们可以使用make_french_deck()为Deck设置默认的属性(Deck.cards)值。
from dataclasses import dataclass
from typing import List
class Deck:
# Will Not Work!!
cards: List[PlayingCard] = make_french_deck()
#生成Deck实例
deck = Deck()
print(deck)
但是,千万不要这么做。这引入了Python中最常见的反常模式:使用可变的默认参数。问题是Deck的所有实例都将使用相同的列表对象作为.cards属性的默认值。 这意味着,如果从一个Deck中移除一张卡,那么它也会从Deck的所有其他实例中消失。 代码运行出错,提示如下
ValueError: mutable default <class 'list'> for field cards is not allowed: use default_factory
实际上,dataclass试图阻止您这样做,并且上面的代码将引发ValueError。相反,dataclass会使用default_factory
来处理可变默认值。使用default_factory,我们可以使用field()来专门指定默认字段:
from dataclasses import dataclass, field
from typing import List
class Deck:
cards: List[PlayingCard] = field(default_factory= make_french_deck)
#生成Deck实例
deck = Deck()
print(deck)
运行如我们预期般的结果
[PlayingCard(rank='2', suit='♣'), PlayingCard(rank='3', suit='♣'), PlayingCard(rank='4', suit='♣'), PlayingCard(rank='5', suit='♣'), PlayingCard(rank='6', suit='♣'),
...
PlayingCard(rank='J', suit='♠'), PlayingCard(rank='Q', suit='♠'), PlayingCard(rank='K', suit='♠'), PlayingCard(rank='A', suit='♠')]
注意defualt_factory参数传入的是make_french_deck,不是make_french_deck()
。如果传入make_french_deck(),代码运行报错
TypeError: 'list' object is not callable
field()
field()用来定义dataclass类的每个字段。之后,我们会看到其他案例。field()参数及其含义(功能):
default: 字段的默认值
default_factory:返回字段初始值的函数
init: 布尔值,默认为True。相当于使用了init()方法
repr:布尔值,默认为True。可以通过print()将实例打印出来
compare:布尔值,默认为True。对不同的实例进行字段的比较。
hash:布尔值,默认为True。计算包含的字段的hash值
metadata:关于该字段的信息的映射
在Position类中,我们之前是通过如
from dataclasses import dataclass
@dataclass
class Position:
name: str
lon: float = 0.0
lat: float = 0.0
使用lat: float = 0.0
设置属性(字段)的默认值。然而,如果使用field()方法,且不想print()打印出来,则需要写成lat:float = field(default = 0.0, repr = False)
参数metadata不是由dataclass类本身使用,但可供您(或第三方软件包)将信息附加到字段。 在Position类中,我们可以指定纬度和经度应以度为单位:
from dataclasses import dataclass, field
class Position:
name: str
lon: float = field(default=0.0, metadata={'unit': 'degrees'})
lat: float = field(default=0.0, metadata={'unit': 'degrees'})
可以使用fields()函数检索metadata(以及有关字段的其他信息)(注意复数s):
from dataclasses import fields
print(fields(Position))
运行
(Field(name='name',type=<class 'str'>,default=<dataclasses._MISSING_TYPE object at 0x104577198>,default_factory=<dataclasses._MISSING_TYPE object at 0x104577198>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),_field_type=_FIELD), Field(name='lon',type=<class 'float'>,default=0.0,default_factory=<dataclasses._MISSING_TYPE object at 0x104577198>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({'unit': 'degrees'}),_field_type=_FIELD), Field(name='lat',type=<class 'float'>,default=0.0,default_factory=<dataclasses._MISSING_TYPE object at 0x104577198>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({'unit': 'degrees'}),_field_type=_FIELD))
表示representation
print(Deck())
运行结果
Deck(cards=[PlayingCard(rank='2', suit='♣'), PlayingCard(rank='3', suit='♣'), ...
PlayingCard(rank='K', suit='♠'), PlayingCard(rank='A', suit='♠')])
虽然Deck()的这种表示是明确的和可读的,但它也非常冗长。 我在上面的输出中删除了Deck上52张卡中的48张。 在80列显示器上,只需打印完整的Deck就占用22行! 让我们添加一个更简洁的表示。 通常,Python对象有两种不同的字符串表示形式:
repr(obj)由obj ._ repr _()定义,并且应该返回对开发人员友好的obj表示。如果可能,这应该是可以重新创建obj的代码。dataclass类执行此操作。
str(obj)由obj._ str__()定义,并应返回obj的用户友好表示。数据类不实现_ str __()方法,因此Python将回退到._ repr _()方法。
让我们实现一个用户友好的PlayCard表示:
from dataclasses import dataclass
class PlayingCard:
rank: str
suit: str
def __str__(self):
return f'{self.suit}{self.rank}'
playing = PlayingCard('Q', 'spade')
print(playing)
运行结果
spadeQ
我们将上述应用到Deck类中
from dataclasses import dataclass, field
from typing import List
RANKS = '2 3 4 5 6 7 8 9 10 J Q K A'.split()
SUITS = '♣ ♢ ♡ ♠'.split()
def make_french_deck():
return [PlayingCard(r, s) for s in SUITS for r in RANKS]
@dataclass
class PlayingCard:
rank: str
suit: str
def __str__(self):
#将显示简化了
return f'{self.suit}{self.rank}'
@dataclass
class Deck:
cards: List[PlayingCard] = field(default_factory= make_french_deck)
#Deck显示大大简化
print(Deck())
运行结果
Deck(cards=[PlayingCard(rank='2', suit='♣'), PlayingCard(rank='3', suit='♣'), PlayingCard(rank='4', suit='♣'), PlayingCard(rank='5', suit='♣'), PlayingCard(rank='6', suit='♣'), PlayingCard(rank='3', suit='♡'), PlayingCard(rank='4', suit='♡'),
......
PlayingCard(rank='8', suit='♠'), PlayingCard(rank='9', suit='♠'), PlayingCard(rank='10', suit='♠'), PlayingCard(rank='J', suit='♠'), PlayingCard(rank='Q', suit='♠'), PlayingCard(rank='K', suit='♠'), PlayingCard(rank='A', suit='♠')])
现在我们看看添加自己的.__ repr __()方法,产生更简洁的Deck表示:
from dataclasses import dataclass, field
from typing import List
class Deck:
cards: List[PlayingCard] = field(default_factory=make_french_deck)
def __repr__(self):
cards = ', '.join(f'{c!s}' for c in self.cards)
return f'{self.__class__.__name__}({cards})'
注意{c!s}格式字符串中的!s说明符。这意味着我们明确要使用PlayingCards的str()表示。使用新的._ repr _(),使得Deck的表示更简洁:
print(Deck())
运行结果
Deck(♣2, ♣3, ♣4, ♣5, ♣6, ♣7, ♣8, ♣9, ♣10, ♣J, ♣Q, ♣K, ♣A,
♢2, ♢3, ♢4, ♢5, ♢6, ♢7, ♢8, ♢9, ♢10, ♢J, ♢Q, ♢K, ♢A,
♡2, ♡3, ♡4, ♡5, ♡6, ♡7, ♡8, ♡9, ♡10, ♡J, ♡Q, ♡K, ♡A,
♠2, ♠3, ♠4, ♠5, ♠6, ♠7, ♠8, ♠9, ♠10, ♠J, ♠Q, ♠K, ♠A)
比较卡牌
在许多纸牌游戏中,卡片是可以相互比较。比如斗地主中,K比Q大。但是PlayingCard类不支持这种比较:
'Q', '♡')
> ace_of_spades = PlayingCard('A', '♠')
> ace_of_spades > queen_of_hearts
TypeError: '>' not supported between instances of 'Card' and 'Card'
> queen_of_hearts = PlayingCard(但在dataclass中,是很容易实现的。
from dataclasses import dataclass
class PlayingCard:
rank: str
suit: str
def __str__(self):
return f'{self.suit}{self.rank}'
@dataclass装饰器有两种形式。 到目前为止,您已经看到了指定@dataclass的简单形式,没有任何括号和参数。 但是,您也可以在括号中为@dataclass()装饰器提供参数。 支持以下参数:
init: Add.init() 方法,默认为True
repr: Add.repr()方法,默认为True
eq: Add.eq()方法,默认为True
order:顺序,默认为False
unsafe_hash:强制添加.hash()方法,默认为False。
frozen: 如果为True,则分配给字段会引发异常。 (默认为False。)
queen_of_hearts = PlayingCard('Q', '♡')
ace_of_spades = PlayingCard('A', '♠')
print(ace_of_spades > queen_of_hearts)
现在可以进行比较,运行结果
False
不可变dataclass类
之前看到的namedtuple的一个定义特征是它是不可变的。 也就是说,其字段的值可能永远不会改变。 对于许多类型的类,这是一个好主意! 要使dataclass类不可变,请在创建时设置frozen = True。 例如,以下是您Position类的不可变版本:
from dataclasses import dataclass
class Position:
name: str
lon: float = 0.0
lat: float = 0.0
pos = Position('Oslo', 10.8, 59.9)
print(pos.name)
pos.name = 'Stockholm'
运行结果
'Oslo'
dataclasses.FrozenInstanceError: cannot assign to field 'name'
类的继承
我们可以非常自由的对dataclass进行子类化(继承操作)。例如,我们将使用country字段扩展Position示例:
from dataclasses import dataclass
class Position:
name: str
lon: float
lat: float
class Capital(Position):
country: str
print(Capital('Ohio', 10.8, 59.9, 'Norway'))
运行结果
Capital(name='Oslo', lon=10.8, lat=59.9, country='Norway')
Capital的country字段是在三个原始字段之后添加的。如果基类中的任何字段具有默认值,事情会变得复杂一些:
from dataclasses import dataclass
class Position:
name: str
lon: float = 0.0
lat: float = 0.0
class Capital(Position):
country: str # Does NOT work
解决办法
from dataclasses import dataclass
class Position:
name: str
lon: float = 0.0
lat: float = 0.0
class Capital(Position):
country: str = 'Unknown'
lat: float = 40.0
然后,Capital中字段的顺序仍然是name,lon,lat,country。但是,lat的默认值为40.0。
print(Capital('Madrid', country='Spain'))
运行正常,返回的结果
Capital(name='Madrid', lon=0.0, lat=40.0, country='Spain')
往期文章
昨日财报
赞赏、点赞、转发、AD支持都是对大邓的认可和支持,希望大家在阅读后顺便帮大邓转发一下。前天的0.14,昨天20.22,大家真给力!